import qs from "qs";

/**
 * 全局和单个接口的公共配置项
 */
export interface BaseConfig {
    //请求URL的前缀，如果传入的url不为完整路径时会自动拼接此参数
    baseURL?: string;
    //请求参数，最终会拼接在URL上
    params?: URLSearchParams | object;
    //params不是URLSearchParams类型时，使用此方法序列化为字符串
    paramsSerializer?: (params: any) => string
    //超时时间
    timeout?: number;
    //提交的数据，最终会被处理为对应的格式，放在body中
    data?: FormData | URLSearchParams | object | string,
    //序列化data的方法，只有当data的类型不为FormData和URLSearchParams时，并且请求方式指定为表单提交时执行
    dataSerializer?: (params: any) => string
    //请求头
    headers?: HeadersInit;
    //请求方式
    method?: string;
    //返回值类型
    responseType?: "arrayBuffer" | "blob" | "formData" | "json" | "text";
}

/**
 * 单个接口可配置项
 */
export interface ApiConfig extends BaseConfig {
    //请求使用的url
    url: string;
}

/**
 * 全局可配置项
 */
export interface GlobalConfig extends BaseConfig {
    successCode?: number;
}

/**
 * 内部使用
 */
interface FetchRequestConfig extends RequestInit, GlobalConfig, ApiConfig {
    //必填参数
    successCode: number;
    //延迟执行任务的id
    timeoutId?: number;
}

export interface Result<T = any> {
    code: number;
    message: string;
    data: T
}

class RequestInfo {
    readonly url: string
    readonly body?: BodyInit | null;
    readonly time: number;

    constructor(url: string, body: BodyInit | undefined | null, time: number) {
        this.url = url;
        this.body = body;
        this.time = time;
    }

    public equals(requestInfo?: RequestInfo): boolean {
        if (!requestInfo) {
            return false
        }
        //比较请求地址是否相同
        if (this.url !== requestInfo.url) {
            return false;
        }
        //比较请求时间，两次请求时间小于指定毫秒值视为相等
        if (Math.abs(this.time - requestInfo.time) > 500) {
            return false;
        }
        if (this.body != null && requestInfo.body !== null) {
            //请求体不为空
            //如果是表单提交，需要比较表单的值
            if (this.body instanceof URLSearchParams && requestInfo.body instanceof URLSearchParams) {
                return this.body.toString() === requestInfo.body.toString()
            } else if (isString(this.body) && isString(requestInfo.body)) {
                return this.body.toString() === String(requestInfo.body).toString()
            } else {
                //只比较表单类型的参数和字符串类型的参数，如果是其他类型一律视为不相等，因为有可能在分片上传文件时会出现别的类型参数
                return false;
            }
        } else {
            return true;
        }
    }
}

function isString(object?: any) {
    return typeof object === 'string' || object instanceof String;
}

export default class FetchService {
    //上次请求的信息
    private lastRequest?: RequestInfo;
    //全局配置参数
    private readonly baseConfig: FetchRequestConfig

    //成功码默认值
    public static readonly SUCCESS_CODE = 200;
    get: <T> (config: ApiConfig) => Promise<Result<T>>;
    post: <T>(config: ApiConfig) => Promise<Result<T>>;
    delete: <T>(config: ApiConfig) => Promise<Result<T>>;
    put: <T>(config: ApiConfig) => Promise<Result<T>>;

    private requestByMethod(method: string) {
        return <T>(config: ApiConfig): Promise<Result> => {
            config.method = method
            return this.request(config.url, config);
        }
    }

    private constructor(globalConfig?: GlobalConfig) {
        this.baseConfig = {
            successCode: FetchService.SUCCESS_CODE,
            url: "",
            ...globalConfig
        };
        this.get = this.requestByMethod("get")
        this.post = this.requestByMethod("post")
        this.put = this.requestByMethod("put")
        this.delete = this.requestByMethod("delete")
    }

    /**
     * 对Content-Type，请求URL和请求参数进行处理
     * @param url 请求地址，如果不是http开头的完整路径，会自动拼接baseUrl路径
     * @param httpConfig 请求接口时额外传入的参数
     */
    public request<T>(url: string, httpConfig?: ApiConfig): Promise<Result<T>> {
        const config: FetchRequestConfig = {
            ...this.baseConfig,
            ...httpConfig
        }
        //转换为Headers类型，方便操作
        const headers: Headers = new Headers(config?.headers);
        if (config.data instanceof FormData) {
            //上传文件时的类型
            headers.set("content-type", "form-data/multipart");
            config.headers = headers;
            config.body = config.data
        } else if (config.data instanceof URLSearchParams) {
            //表单提交
            headers.set("content-type", "application/x-www-form-urlencoded");
            config.headers = headers;
            //URLSearchParams转字符串
            config.body = config.data.toString();
        } else {
            const contentType = headers.get("content-type")
            if (contentType === 'application/x-www-form-urlencoded') {
                //表单提交
                if (config.dataSerializer) {
                    config.body = config.dataSerializer(config.data)
                } else {
                    config.body = qs.stringify(config.data, {allowDots: true});
                }
            } else if (contentType === 'application/json') {
                config.body = JSON.stringify(config.data)
            } else {
                throw new Error(`不支持的请求方式：${contentType}`)
            }
        }
        //对url和params进行处理
        let strings = url.split('?');
        //url去除参数部分
        url = strings[0];
        //url上的参数
        let urlSearchParams = new URLSearchParams(strings[1]);
        let queryString = urlSearchParams.toString();
        let paramStr;
        if (config.params instanceof URLSearchParams) {
            paramStr = config.params.toString()
        } else {
            if (config.paramsSerializer) {
                //将params参数转字符串
                paramStr = config.paramsSerializer(config.params) || '';
            } else {
                paramStr = qs.stringify(config.params, {allowDots: true}) || '';
            }
        }
        if (queryString && paramStr) {
            queryString = queryString + "&" + paramStr;
        } else {
            queryString = queryString + paramStr
        }
        if (queryString) {
            url = url + "?" + queryString;
        }
        if (url.startsWith("http://") || url.startsWith("https://")) {
            return this.customFetch<T>(url, config);
        } else {
            if (config.baseURL) {
                url = config.baseURL + url;
            }
            return this.customFetch<T>(url, config);
        }
    }

    /**
     * 请求拦截器，重复提交，响应拦截，超时取消
     * @param url
     * @param init
     * @private
     */
    private async customFetch<T>(url: string, init: FetchRequestConfig): Promise<any> {
        //请求拦截处理
        init = this.handlerRequest(init);
        if (init.timeout) {
            //用于取消请求（实现超时功能）
            const controller = new AbortController();
            init.signal = controller.signal
            //超时取消，时间设置为负数表示不超时（单位毫秒）
            init.timeoutId = setTimeout(() => controller.abort(), init.timeout);
        }

        let requestInfo = new RequestInfo(url, init.body, new Date().getTime());
        if (requestInfo.equals(this.lastRequest)) {
            return Promise.reject("请不要重复点击")
        }
        //发起fetch请求
        const res = await fetch(url, init).then(async (response: Response): Promise<string | Result<T>> => {
            if (response.ok) {
                //表示网络上请求成功了，也就是http请求是成功了，但是具体的业务层是否成功后续根据正文中的code判断
                let result: Result;
                let responseType = init.responseType;
                if (responseType === 'arrayBuffer') {
                    result = {
                        code: 200,
                        message: 'OK',
                        data: await response.arrayBuffer()
                    };
                } else if (responseType === 'blob') {
                    result = {
                        code: 200,
                        message: 'OK',
                        data: await response.blob()
                    };
                } else if (responseType === 'formData') {
                    result = {
                        code: 200,
                        message: 'OK',
                        data: await response.formData()
                    };
                } else if (responseType === 'text') {
                    result = {
                        code: 200,
                        message: 'OK',
                        data: await response.text()
                    }
                } else {
                    result = await response.json();
                }
                return result
            } else {
                //开发环境没有启动后端接口服务，会走到这里 Internal Server Error
                return response.statusText
            }
        }).catch(error => {
            //处理请求发生的错误，比如请求超时等
            if (error instanceof Error) {
                return error.message
            } else {
                return String(error);
            }
        }).finally(() => {
            if (init.timeoutId) {
                //取消延时任务
                clearTimeout(init.timeoutId);
            }
            //保留上次请求信息
            this.lastRequest = requestInfo
        });
        //这里的res只有字符串和Result两种格式，如果是字符串则表面请求失败
        if (isString(res)) {
            const str = <string>res;
            if (str === 'The user aborted a request.') {
                return this.handlerError<T>("Request Timeout");
            }
            return this.handlerError(str);
        } else {
            return this.handlerSuccess(<Result>res);
        }
    }

    /**
     * 拦截请求
     * @param config
     * @private
     */
    private handlerRequest<T>(config: FetchRequestConfig): FetchRequestConfig {
        if (this._requestInterceptor) {
            return Object.assign(config, this._requestInterceptor(config))
        }
        return config;
    }

    /**
     * 这里的成功指的是http请求上成功了，但是具体的业务层逻辑是否成功自行在拦截器处理
     * result中的code可能并不一定为200
     * @param result
     * @private
     */
    private handlerSuccess<T>(result: Result<T>) {
        if (this._responseInterceptor) {
            let r = this._responseInterceptor(result);
            if (r instanceof Promise) {
                return r;
            } else {
                return Promise.resolve<Result<T>>(this._responseInterceptor(result));
            }
        } else {
            //此处进行响应拦截处理
            return Promise.resolve<Result<T>>(result);
        }
    }

    /**
     * http请求失败
     * @param reason 错误原因
     * @private
     */
    private handlerError<T>(reason: string): Promise<string> {
        if (this._responseErrorInterceptor) {
            let r = this._responseErrorInterceptor(reason);
            if (r instanceof Promise) {
                return r;
            } else {
                return Promise.reject<string>(this._responseErrorInterceptor(reason));
            }
        }
        return Promise.reject<string>(reason);
    }

    /**
     * 创建一个实例
     * @param init
     */
    public static create(init?: GlobalConfig) {
        return new FetchService(init)
    }

    private _requestInterceptor?: (init: GlobalConfig) => GlobalConfig
    /**
     * 响应拦截器，执行此回调时，表明当前http请求已经成功，但是在业务上并非成功需要在此拦截器进行判断
     * 如果需要判定为操作失败的请求，则返回reject状态的Promise
     * 如果需要判定为操作成功，则返回原值或resolve的Promise
     * 如果操作成功，可以对
     */
    private _responseInterceptor?: <T> (result: Result<T>) => Result<T> | Promise<Result<T>>
    private _responseErrorInterceptor?: <T> (reason: string) => string | Promise<string>

    set requestInterceptor(value: (init: GlobalConfig) => GlobalConfig) {
        this._requestInterceptor = value;
    }

    set responseInterceptor(value: <T>(result: Result<T>) => (Result<T> | Promise<Result<T>>)) {
        this._responseInterceptor = value;
    }

    set responseErrorInterceptor(value: <T>(reason: string) => (string | Promise<string>)) {
        this._responseErrorInterceptor = value;
    }
}